package com.hmdp.utils;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;

/**
 *  Redis工具类封装
 * @BelongsProject:hm-dianping
 * @BelongsPackage:com.hmdp.utils
 * @Author:zouchangjin
 * @CreateTime:2025-07-08 01:30
 * @Version:1.0
 */
@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    /**
     * 手动创建线程池
     */
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * @param key redis的key
     * @param value redis的值
     * @param expire redis的过期时间
     * @param timeUnit redis时间的单位
     */
    public void set(String key, Object value, Long expire, TimeUnit timeUnit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), expire, timeUnit);
    }

    /**
     * 设置缓存时间，添加属性中的缓存时间，不是这个redis的key的过期时间
     * @param key redis的key
     * @param value redis的值
     */
    public void setWithLogicalExpire(String key, Object value, Long expire, TimeUnit timeUnit){
        //封装成RedisData对象,设置redis的逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(expire)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }


    public <R,ID> R get(String keyPrefix, ID id, Class<R> clazz, Function<ID,R> dbFallBack, Long expire, TimeUnit timeUnit){
        //redis中的key
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        //存在，返回
        if(StrUtil.isNotBlank(json)){
            return JSONUtil.toBean(json, clazz);
        }
        //判断是否为空值
        if(null != json){
            //返回错误信息
            return null;
        }

        //根据id查询数据库
        R r = dbFallBack.apply(id);
        this.set(key,r,expire,timeUnit);

        return r;
    }

    /**
     *
     * @param keyPrefix key前缀
     * @param id    数据id
     * @param tClass 数据类型
     * @param dbFallBack 回调的函数
     * @param expire    过期时间
     * @param timeUnit  时间单位
     * @return 数据类型
     * @param <T> 泛型，什么类型传过来 返回什么
     * @param <ID> 泛型，id（可能是String 可能是Long）
     */
    public <T,ID> T queryWithLogicalExpire(String keyPrefix,ID id,Class<T> tClass,Function<ID,T> dbFallBack, Long expire, TimeUnit timeUnit) {
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);
        //2.不存在,直接返回空
        if(StrUtil.isBlank(json)){
            return null;
        }
        //转换成RedisData对象
        RedisData bean = JSONUtil.toBean(json, RedisData.class);
        //获取到RedisData中的data属性，转换成需要的实体
        T t = JSONUtil.toBean((JSONObject) bean.getData(),tClass);
        //获取设置的缓存时间
        LocalDateTime expireTime = bean.getExpireTime();
        //判断缓存时间是否在当前时间之前
        if(expireTime.isAfter(LocalDateTime.now())){
            return t;
        }

        //过期，重建缓存，获取互斥锁
        if(tryLock(key)){
            //如果存在表示获取到了锁，redis第一次添加这个key值，则新开线程去执行redis缓存操作,需保证释放锁
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //查询数据库
                    T apply = dbFallBack.apply(id);
                    //
                    this.setWithLogicalExpire(key,apply,expire,timeUnit);
                }catch (Exception e){
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unlock(LOCK_SHOP_KEY+id);
                }
            });
        }
        return t;
    }


    /**
     * 添加锁
     * @param key redis的key
     * @return 判断是否存在
     */
    private boolean tryLock(String key){
        //setIfAbsent方法，如果key存在则返回，不存在则返回false
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent( key, key, LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(flag);
    }

    /**
     * 删除锁
     * @param key redis的key
     */
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }

}
